| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222 |
- import '../../style.scss';
- import { cache } from 'react';
- import { Metadata } from 'next';
- import Link from 'next/link';
- import { notFound } from 'next/navigation';
- import { fetchBondDetail } from '@/lib/api/seibro';
- import { codeLabel, formatAmountCompact, formatDecimal, formatRatePercent, formatShares, dateOrDash } from '@/lib/utils/market';
- import {
- BOND_OPTION_LABEL,
- BOND_RATE_CHG_LABEL,
- BOND_GUARANTEE_LABEL,
- BOND_RANK_LABEL,
- BOND_INT_PAY_WAY_LABEL,
- BOND_INT_CYCLE_LABEL,
- BOND_INT_TIMING_LABEL,
- BOND_PARTICUL_KIND_LABEL,
- BOND_SECN_KACD_LABEL
- } from '@/types/seibro';
- type Props = {
- params: Promise<{
- isin: string;
- }>;
- };
- // generateMetadata + 본문 이중 호출 방지 (요청 단위 dedupe)
- const getDetail = cache(async (isin: string) => {
- return await fetchBondDetail(isin);
- });
- export async function generateMetadata({ params }: Props): Promise<Metadata>
- {
- const { isin } = await params;
- const res = await getDetail(isin);
- const name = res.success && res.data?.master?.name ? res.data.master.name : isin;
- return {
- title: `${name} 채권 상세 — 개미투자`,
- description: `${name}(${isin}) 채권 상세 — 만기·표면이자율·신용등급·이자지급·종가/YTM (SEIBro)`
- };
- }
- export default async function BondDetailPage({ params }: Props)
- {
- const { isin } = await params;
- const res = await getDetail(isin);
- if (!res.success && res.status === 404) {
- return notFound();
- }
- if (!res.success || !res.data) {
- return (
- <div className='market'>
- <Link href='/market/bonds' className='market__detail-back'>← 채권 목록</Link>
- <div className='market__error' role='alert'>
- <strong>채권 정보를 불러오지 못했습니다.</strong>
- 잠시 후 다시 시도해 주세요.
- </div>
- </div>
- );
- }
- const detail = res.data;
- const master = detail.master;
- const interest = detail.interest;
- const market = detail.market;
- const name = master?.name ?? detail.isin;
- const grades = [
- { agency: 'KIS', grade: master?.kisGrade },
- { agency: 'NICE', grade: master?.niceGrade },
- { agency: 'SCI', grade: master?.sciGrade },
- { agency: 'KR', grade: master?.krGrade }
- ].filter(c => c.grade);
- return (
- <div className='market'>
- <Link href='/market/bonds' className='market__detail-back'>← 채권 목록</Link>
- <div className='market__head'>
- <h1 className='market__title'>{name}</h1>
- <span className='market__basis'>{detail.isin} · 채권 상세 (SEIBro)</span>
- </div>
- {/* KRX 최신 시세 */}
- {market && (
- <div className='market__spec-card'>
- <h2 className='market__spec-card-title'>최신 시세 <span className='market__basis'>({market.tradeDate} 기준)</span></h2>
- <dl className='market__spec-grid'>
- <div className='market__spec-item'>
- <dt>종가</dt>
- <dd>{formatDecimal(market.close)}</dd>
- </div>
- <div className='market__spec-item'>
- <dt>만기수익률(YTM)</dt>
- <dd>{formatRatePercent(market.yieldToMaturity, 3)}</dd>
- </div>
- <div className='market__spec-item'>
- <dt>거래량</dt>
- <dd>{formatShares(market.volume)}</dd>
- </div>
- </dl>
- </div>
- )}
- {/* 채권 마스터 */}
- {master ? (
- <div className='market__spec-card'>
- <h2 className='market__spec-card-title'>발행 정보</h2>
- <dl className='market__spec-grid'>
- <div className='market__spec-item'>
- <dt>종목종류</dt>
- <dd>{codeLabel(master.secnKacd, BOND_SECN_KACD_LABEL)}</dd>
- </div>
- <div className='market__spec-item'>
- <dt>발행일</dt>
- <dd>{dateOrDash(master.issueDate)}</dd>
- </div>
- <div className='market__spec-item'>
- <dt>만기일</dt>
- <dd>{dateOrDash(master.maturityDate)}</dd>
- </div>
- <div className='market__spec-item'>
- <dt>표면이자율</dt>
- <dd>{formatRatePercent(master.couponRate, 3)}</dd>
- </div>
- <div className='market__spec-item'>
- <dt>통화</dt>
- <dd>{master.currency ?? '—'}</dd>
- </div>
- <div className='market__spec-item'>
- <dt>최초발행금액</dt>
- <dd>{formatAmountCompact(master.firstIssueAmount)}</dd>
- </div>
- <div className='market__spec-item'>
- <dt>발행잔액</dt>
- <dd>{formatAmountCompact(master.issueRemaining)}</dd>
- </div>
- <div className='market__spec-item'>
- <dt>금리유형</dt>
- <dd>{codeLabel(master.rateChangeTpcd, BOND_RATE_CHG_LABEL)}</dd>
- </div>
- <div className='market__spec-item'>
- <dt>보증</dt>
- <dd>{codeLabel(master.guaranteeTpcd, BOND_GUARANTEE_LABEL)}</dd>
- </div>
- <div className='market__spec-item'>
- <dt>순위</dt>
- <dd>{codeLabel(master.rankTpcd, BOND_RANK_LABEL)}</dd>
- </div>
- <div className='market__spec-item'>
- <dt>이자지급방법</dt>
- <dd>{codeLabel(master.intPayWayTpcd, BOND_INT_PAY_WAY_LABEL)}</dd>
- </div>
- <div className='market__spec-item'>
- <dt>옵션</dt>
- <dd>{codeLabel(master.optionTpcd, BOND_OPTION_LABEL)}</dd>
- </div>
- <div className='market__spec-item'>
- <dt>특이종류</dt>
- <dd>{codeLabel(master.particularBondKindTpcd, BOND_PARTICUL_KIND_LABEL)}</dd>
- </div>
- <div className='market__spec-item'>
- <dt>조기상환</dt>
- <dd>{master.forcedEarlyRedemption === null ? '—' : master.forcedEarlyRedemption ? '있음' : '없음'}</dd>
- </div>
- <div className='market__spec-item'>
- <dt>전자증권</dt>
- <dd>{master.isElectronicSecurity === null ? '—' : master.isElectronicSecurity ? '전자' : '예탁'}</dd>
- </div>
- </dl>
- {grades.length > 0 && (
- <div className='market__grades'>
- {grades.map(c => (
- <div key={c.agency} className='market__grade'>
- <span>{c.agency}</span>
- <strong>{c.grade}</strong>
- </div>
- ))}
- </div>
- )}
- </div>
- ) : (
- <div className='market__empty'>채권 발행 마스터 정보가 없습니다 (KRX 시세만 수집됨).</div>
- )}
- {/* 이자지급 정보 */}
- {interest && (
- <div className='market__spec-card'>
- <h2 className='market__spec-card-title'>이자지급</h2>
- <dl className='market__spec-grid'>
- <div className='market__spec-item'>
- <dt>표면이자율</dt>
- <dd>{formatRatePercent(interest.couponRate, 3)}</dd>
- </div>
- <div className='market__spec-item'>
- <dt>지급방법</dt>
- <dd>{codeLabel(interest.intPayWayTpcd, BOND_INT_PAY_WAY_LABEL)}</dd>
- </div>
- <div className='market__spec-item'>
- <dt>지급주기</dt>
- <dd>{interest.cycleTerms ?? '—'}{interest.cycleTpcd ? codeLabel(interest.cycleTpcd, BOND_INT_CYCLE_LABEL) : ''}</dd>
- </div>
- <div className='market__spec-item'>
- <dt>지급시기</dt>
- <dd>{codeLabel(interest.payTimingTpcd, BOND_INT_TIMING_LABEL)}</dd>
- </div>
- <div className='market__spec-item'>
- <dt>직전 이표일</dt>
- <dd>{dateOrDash(interest.beforeCouponDate)}</dd>
- </div>
- <div className='market__spec-item'>
- <dt>차기 이표일</dt>
- <dd>{dateOrDash(interest.afterCouponDate)}</dd>
- </div>
- </dl>
- </div>
- )}
- </div>
- );
- }
|